#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//第19讲：自定义类型：结构体+第20讲：⾃定义类型：联合和枚举
//2. 结构体内存对⻬
//现在我们深⼊讨论⼀个问题：计算结构体的⼤⼩。
//2.1 对⻬规则
//1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
//2. 其他成员变量要对⻬到某个数字（对⻬数）的整数倍的地址处。
//对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩中的较⼩值。- VS 中默认的值为 8 - Linux中 gcc 没有默认对⻬数，对⻬数就是成员⾃⾝的⼤⼩
//3. 结构体总⼤⼩为最⼤对⻬数（结构体中每个成员变量都有⼀个对⻬数，所有对⻬数中最⼤的）的整数倍。

//练习2
//struct S2
//{
//	char c1;//第一个成员：大小为1 小于8，对齐数就为1，对齐到以1为倍数的地址处
//	char c2;//第二个成员:对齐数1，对齐到以1为倍数的地址处
//	int i;//对齐数4,对齐到以4为倍数的地址处
//	//4是最大的对齐数，所以S2结构体变量的值是4的倍数
//};
//int main()
//{ 
//	printf("%zu\n", sizeof(struct S2));
//}

//练习3
//struct S3
//{
//	double d;//对齐数为8,存入0~7地址处
//	char c;//对齐数为1,存入8地址处
//	int i;//对齐数为4,存入12~15地址处 从12开始存放，12之前是0（结构体变量起始位置）~11，共12个字节的偏移量，是4的倍数
//	//8是最大的对齐数，那这个结构体变量的大小就是8的倍数,就是0~15 共16个字节
//};
//int main()
//{
//	printf("%zu\n", sizeof(struct S3));
//}

//练习4-结构体嵌套问题
//接下来引入第4条规则:
//4. 如果嵌套了结构体的情况，嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处，结构
//体的整体⼤⼩就是所有最⼤对⻬数（含嵌套结构体中成员的对⻬数）的整数倍。
//struct S3
//{
//	double d;
//	char c;
//	int i; 
//};//大小为16
//struct S4
//{
//	char c1;//对齐数为1: 0
//	struct S3 s3;//结构体嵌套结构体，对齐数就是自己成员当中的最大对齐数：8，那就从8的倍数开始存:8~23(大小为16)
//	double d;//对齐数为8(从8的倍数处开始存):24~31
//	//结构体的整体⼤⼩就是所有最⼤对⻬数（含嵌套结构体中成员的对⻬数）的整数倍。
//	//最大对齐数:8 所以这个结构体大小就是8的整数倍 32 -- (0~31)
//};
//int main()
//{
//	printf("%zu\n", sizeof(struct S4));
//}


//2.2 为什么存在内存对⻬?
//1. 平台原因 (移植原因)：
//不是所有的硬件平台都能访问任意地址上的任意数据的；某些硬件平台只能在某些地址处取某些特定
//类型的数据，否则抛出硬件异常。
//2. 性能原因：
//数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于，为了访问未对⻬的内存，处理器需要
//作两次内存访问；⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节，则地
//址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数，那么就可以
//⽤⼀个内存操作来读或者写值了。否则，我们可能需要执⾏两次内存访问，因为对象可能被分放在两
//个8字节内存块中
//总体来说：结构体的内存对⻬是拿空间来换取时间的做法。
//那在设计结构体的时候，我们既要满⾜对⻬，⼜要节省空间应该让占⽤空间⼩的成员尽量集中在⼀起


//2.3 修改默认对⻬数
//#pragma 这个预处理指令，可以改变编译器的默认对⻬数。
//#pragma pack(1)//设置默认对⻬数为1
//struct S
//{
//	char c1;
//	int i;
//	char c2;
//};
//#pragma pack()//取消设置的对⻬数，还原为默认
//int main()
//{
//	printf("%d\n", sizeof(struct S));
//	return 0;
//}
//结构体在对⻬⽅式不合适的时候，我们可以⾃⼰更改默认对⻬数



//3. 结构体传参
//先来看两种不同的传参方式:
//1.值传递:
//struct S
//{
//	int data[1000];
//	int num;
//};
//struct S s = { {1,2,3,4}, 1000 };
//void print1(struct S s)
//{
//	for (int i = 0; i < 4; i++)
//	{
//		printf("%d ", s.data[i]);
//	}
//	printf("%d\n", s.num);
//}
//int main()
//{
//	print1(s);
//	return 0;
//}
//这样虽然能完成任务但是有两个缺点:
//1.结构体占用内存太大，采用值传递需要再临时对结构体变量进行拷贝，浪费空间
//2.拷贝过程浪费时间
//总结:值传递造成时间和空间上的浪费

//2.地址传递:
//struct S
//{
//	int data[1000];
//	int num;
//};
//struct S s = { {1,2,3,4}, 1000 };
//void print2(struct S* ps)
//{
//	for (int i = 0; i < 4; i++)
//	{
//		printf("%d ", s.data[i]);
//	}
//	printf("%d\n", ps->num);
//}
//int main()
//{
//	print2(&s);
//	return 0;
//}
//地址传递的好处:
//仅仅把指针变量传递过去，而指针变量只占4个或者8个字节，不仅占用空间小，而且无临时拷贝效率更高
//结论：
//结构体传参的时候，要传结构体的地址。



//4. 结构体实现位段
//4.1 什么是位段
//位段的声明和结构是类似的，有两个不同：
//1. 位段的成员必须是 int、unsigned int 或signed int ，在C99中位段成员的类型也可以选择其他类型。
//2. 位段的成员名后边有⼀个冒号和⼀个数字。
//struct A
//{
//	int _a : 2;//占2个比特位     先开辟一个整型大小:4字节32比特位
//	int _b : 5;//占5个比特位		// 2+5+10=17 如果再存放_d的话就超出32位
//	int _c : 10;//占10个比特位	//所以存放_d的时候需要再开辟一个int类型的大小
//	int _d : 30;//占30个比特位	// 第一个int存放_a _b _c 第二个int存放 _d所以占8个字节大小
//};
//int main()
//{
//	printf("%zu\n", sizeof(struct A));
//	return 0;
//}
//位段是专门用来节省内存的


//4.2 位段的内存分配
//1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
//2. 位段的空间上是按照需要以4个字节（ int ）或者1个字节（ char ）的⽅式来开辟的。
//3. 位段涉及很多不确定因素，位段是不跨平台的，注重可移植的程序应该避免使⽤位段。
//注意:
//1.申请到的一块内存中，从左向右使用还是从右向左使用是不确定的（vs上是从右向左使用）
//2.剩余的空间，不足下一个成员使用，是浪费呢？还是继续使用？(vs上选择浪费)
//struct S
//{
//	char a : 3; //先开辟一个一个char类型大小，即8个比特位
//	char b : 4; //用来存放 a和b
//	char c : 5;//后续不够存放c 接着再往后开辟8个比特位来存放c
//	char d : 4;//后续不够存放d 接着再往后开辟8个比特位来存放d
//};				//所以一共占3个字节
//int main()
//{
//	struct S s = { 0 };//其余被浪费的空间都是0
//	s.a = 10;//只给a开辟了3个bit位呀，但是10的二进制表示形式为: 0000 1010,所以发生截断，只保留低位 010
//	s.b = 12;//b占4个比特位,12的二进制: 0000 1100保留低位:1100
//	s.c = 3;//c占5个比特位,3的二进制:0000 0011 保留低位:00011
//	s.d = 4;//d占4个比特位,4的二进制: 0000 0100 保留低位:0100
//	//所以s在内存中的表示为:
//	// 0 1100 010 ---- 000 00011 ---- 0000 0100   (共占3个字节)
//	// x   b   a       xxx   c        xxxx   d
//	// 第一个字节       第二个字节      第三个字节
//	//x代表被浪费的空间被0补充 用----分隔不同的字节
//	//用16进制表示就是: 0x62 03 04
//	printf("%zu", sizeof(s));
//}


//4.3 位段的跨平台问题
//1. int 位段被当成有符号数还是⽆符号数是不确定的
//2. 位段中最⼤位的数⽬不能确定。（16位机器最⼤16，32位机器最⼤32，写成27，在16位机器会出问题。
//3. 位段中的成员在内存中从左向右分配，还是从右向左分配，标准尚未定义。
//4. 当⼀个结构包含两个位段，第⼆个位段成员⽐较⼤，⽆法容纳于第⼀个位段剩余的位时，是舍弃剩余的位还是利⽤，这是不确定的。
//总结：
//跟结构相⽐，位段可以达到同样的效果，并且可以很好的节省空间，但是有跨平台的问题存在。



//4.5 位段使⽤的注意事项
//使用位端的时候尽量保证结构体中的成员数据类型一样,否则又会带来一些不确定的东西
//位段的⼏个成员共有同⼀个字节，这样有些成员的起始位置并不是某个字节的起始位置，那么这些位
//置处是没有地址的。内存中每个字节分配⼀个地址，⼀个字节内部的bit位是没有地址的。
//所以不能对位段的成员使⽤& 操作符，这样就不能使⽤scanf直接给位段的成员输⼊值，只能是先输⼊
//放在⼀个变量中，然后赋值给位段的成员。
//struct A
//{
//	int _a : 2;
//	int _b : 5;
//	int _c : 10;
//	int _d : 30;
//};
//int main()
//{
//	struct A sa = { 0 };
//	scanf("%d", &(sa._b));//这是错误的
//	//_b位于一个字节里中间的某个比特位，但是字节内部的bit位是没有地址的
//	//正确的⽰范
//	int b = 0;
//	scanf("%d", &b);
//	sa._b = b;
//	return 0;
//}



//第20讲：⾃定义类型：联合和枚举
//自定义类型:结构体 联合体 枚举
//1. 联合体
//1.1 联合体类型的声明
//联合类型的声明
//union Un
//{
//	char c;
//	int i;//联合体的大小至少是最大成员的大小，即该联合体占4个字节大小
//};
//int main()
//{
//	//联合变量的定义
//	union Un un = { 0 };
//	//计算联合体变量的⼤⼩
//	printf("%d\n", sizeof(un));
//	return 0;
//}
//为什莫联合体变量大小是4呢？
//联合体的大小至少是最大成员的大小，即该联合体占4个字节大小


//1.2 联合体的特点
//联合的成员是共⽤同⼀块内存空间的，这样⼀个联合变量的⼤⼩，⾄少是最⼤成员的⼤⼩（因为联合
//⾄少得有能⼒保存最⼤的那个成员）。
//代码1:
//union Un
//{
//	char c;
//	int i;
//};
//int main()
//{
//	//联合变量的定义
//	union Un un = { 0 };
//	// 下⾯输出的结果是⼀样的吗？
//	printf("%p\n", &(un.i));
//	printf("%p\n", &(un.c));
//	printf("%p\n", &un);
//	//可以看到输出的地址是一模一样的
//	return 0;
//}

//代码2
//union Un
//{
//	char c;
//	int i;
//};
//int main()
//{
//	union Un un = { 0 };
//	un.i = 0x11223344;
//	un.c = 0x55;
//	printf("%x\n", un.i);
//	return 0;
//}
//从上面两段代码中我们可以得到这两条信息:
//1.联合体本身和他的成员的地址都相同
//2.改变联合体其中一个成员的值，另一个成员的值居然也跟着改变惹！！！
//从而我们可以的出来一个结论：联合体的成员是公用一个内存空间的
//因此，联合体也被称为：共用体



//1.4 联合体⼤⼩的计算
//• 联合的⼤⼩⾄少是最⼤成员的⼤⼩。
//注意！！！！！
//联合体的大小是不一定等于最大成员的大小的，它只是至少等于最大成员的大小
//当最大成员的大小不是最大对齐数的整数倍的时候，就要对齐到最大对齐数的整数倍
//union Un1
//{
//	char c[5];//虽然这是个char类型的数组，大小为5个字节，但其本质就是五个char类型的数据，所以他的对齐数还是1
//	int i;//对齐数4
//	//最大成员大小:5 不是最大对齐数的整数倍
//	//联合体的大小也必须是最大对齐数的整数倍:8
//};
//union Un2
//{
//	short c[7];
//	int i;
//};
//int main()
//{
//	//下⾯输出的结果是什么？
//	printf("%d\n", sizeof(union Un1));
//	printf("%d\n", sizeof(union Un2));
//	return 0;
//}


//使⽤联合体是可以节省空间的，举例：
//⽐如，我们要搞⼀个活动，要上线⼀个礼品兑换单，礼品兑换单中有三种商品：图书、杯⼦、衬衫。
//每⼀种商品都有：库存量、价格、商品类型和商品类型相关的其他信息。
//图书：书名、作者、⻚数
//杯⼦：设计
//衬衫：设计、可选颜⾊、可选尺⼨
//图书、杯子、衬衫都有各自的商品类型
//那如果我们想要存放所有信息的话，是不是就可以用联合体来定义图书、杯子、衬衫的商品类型
//如果我们要取出 图书的所有信息，就需要库存量，价格，商品类型：书名、作者、页数。但是其他数据像设计就不需要
//同理，杯子、衬衫都有些商品类型不需要，我们就可以用联合体定义商品类型，需要取出哪个商品的时候就访问这个商品的商品类型就可以了
//struct gift_list
//{
//	int stock_number;//库存量
//	double price; //定价
//	int item_type;//商品类型
//	//这些都是公共属性
//	union {
//		struct
//		{
//			char title[20];//书名
//			char author[20];//作者
//			int num_pages;//⻚数
//		}book;//书的商品类型的属性
//		struct
//		{
//			char design[30];//设计
//		}mug;//杯子的商品类型的属性
//		struct
//		{
//			char design[30];//设计
//			int colors;//颜⾊
//			int sizes;//尺⼨
//		}shirt;//衬衫的商品类型的属性
//	}item;//这些都是各自的属性
//};


//1.5 联合的⼀个练习
//写⼀个程序，判断当前机器是⼤端？还是⼩端？
//int check()
//{
//	union S
//	{
//		char c;
//		int i;
//	}s;
//	s.i = 1;
//	return s.c;//访问的是低地址！！
//}
//int main()
//{
//	int ret = check;
//	if (ret)
//		printf("小端");
//	else
//		printf("大端");
//}



